package com.deltapunkt.proxy;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

import com.deltapunkt.proxy.Command.Type;

//client:connect(target)->auth(target)->xchg ; the target here is the ServerAuthFwdProxy
//server:auth(client)->connect(target)->xchg ; the client here is the ClientAuthFwdProxy
public class ServerAuthPortProxy implements Proxy {
	private final SocketChannel client;
	private SocketChannel target;
	private final BlockingDeque<Event> readQueue;
	private final AsyncServer server;
	private State state;
	private final SocketAddress targetAddress;
	private byte[] clientResponse;
	private int clientIdx;
	private byte[] expectedResponse;

	public ServerAuthPortProxy(AsyncServer server, SocketChannel client,
			SocketAddress targetAddress) {
		this.server = server;
		this.client = client;
		this.targetAddress = targetAddress;
		readQueue = new LinkedBlockingDeque<Event>(10);
		state = new State("init");
	}

	public void read(Event evt) {
		try {
			readQueue.putLast(evt);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private void write(Message m) {
		try {
			server.addMessage(m);
			server.addCommand(new Command(Type.SETOPS, m.getChannel(),
					SelectionKey.OP_WRITE));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public synchronized void run() {
		if (state.is("init")) {
			state = new State("authenticating");
			sendChallenge(); // to client
		} else if (state.is("authenticating")) {
			readResponse(); // from client
			if (state.is("authenticated")) {
				state = new State("connecting");
				initTargetConnection();
			}
		} else if (state.is("connecting")) {
			finishConnection();
		} else if (state.is("connected")) {
			forwardMessage();
		}
	}

	private void sendChallenge() {
		double challenge = Math.random();
		expectedResponse = Secret.generate(challenge);
		clientResponse = new byte[expectedResponse.length];
		write(new Message(client, challenge + "\n"));
		// server.addCommand(new Command(Type.SETOPS, client,
		// SelectionKey.OP_READ));
	}

	private void readResponse() {
		Message m = (Message) readQueue.poll();
		for (byte b : m.getContent()) {
			if (clientIdx < clientResponse.length) {
				clientResponse[clientIdx++] = b;
			} else {
				break;
			}
		}

		if (clientIdx == clientResponse.length) {
			boolean authSuccess = Secret.verify(expectedResponse,
					clientResponse);
			if (authSuccess) {
				state = new State("authenticated");
				server.addCommand(new Command(Type.SETOPS, client)); // tell
																		// client
																		// to
																		// wait
																		// while
																		// connection
																		// with
																		// the
																		// target
																		// is
																		// being
																		// established
			} else {
				terminateConnection(client);
			}
		}
	}

	private void finishConnection() {
		Event e = readQueue.poll();
		if (e != null && e instanceof State) {
			state = (State) e;
			System.out.println("STATE=====" + state + " target=" + target);
			if (state.is("connected")) {
				byte[] ack = new byte[] { (byte) 0xFF };
				write(new Message(client, ack));
				server.addCommand(new Command(Type.SETOPS, target,
						SelectionKey.OP_READ));
			}
		}
	}

	private void forwardMessage() {
		Event e = readQueue.poll();
		if (e != null && e instanceof Message) {
			Message m = (Message) e;
			if (m != null) {
				if (m.getChannel() == client) {
					processFromClient(m);
				}
				if (m.getChannel() == target) {
					processFromTarget(m);
				}
			}
		}
	}

	private void terminateConnection(SocketChannel s) {
		server.addCommand(new Command(Type.TERMINATE, s));
		readQueue.clear();
	}

	private void initTargetConnection() {
		try {
			target = SocketChannel.open();
			target.configureBlocking(false);
			target.connect(targetAddress);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		// add register command
		server.mapSocket(target, this);
		Command cmd = new Command(Command.Type.REGISTER, target,
				SelectionKey.OP_CONNECT);
		server.addCommand(cmd);
		// target == new Connection(destHost, destPort);
		// initiate here connection
		System.out.println("Target connection initiated " + target);
	}

	private void processFromClient(Message m) {
		write(new Message(target, m.getContent()));
	}

	private void processFromTarget(Message m) {
		write(new Message(client, m.getContent()));
	}
}
